import java.util.Collection;
import java.util.LinkedList;
import java.util.Map;
import java.util.HashMap;

import org.json.JSONObject;
import org.json.JSONArray;
import org.json.JSONException;

/**
 * A place for clients to congregate when they are editing the same document.
 *
 * This corresponds to one document in the real world, but perhaps confusingly
 * we store no actual data for it here; this is just to keep track of clients.
 *
 * The static sections of this class deal with accessing <code>Document</code>s
 * by id, which is how we need to find them when a client subscribes.
 */
public class Document
{

/* Code for the global set of documents */

    private static Map<String,Document> map = new HashMap<String,Document>();
    
    /**
     * Retrive a <code>Document</code> based on the given id. 
     *
     * We assume that any id is valid, and return a new <code>Document</code>
     * when we don't already have one that matches.
     *
     * A cleaner interface might be to have a factory that can make an informed
     * decision as to whether the id is valid.
     *
     * @param id  The document id (recieved from the client)
     * @return    A <code>Document</code>
     */
    public static Document get(String id)
    {
        if (! map.containsKey(id))
            map.put(id, new Document(id));
        return map.get(id);
    }

    /**
     * Remove a <code>Document</code> from the map.
     *
     * This should be called when no clients are currently subscribed to the
     * <code>Document</code> lest the <code>Map</code> grow indefinitely.
     *
     * XXX: This MUST only be called when the document has no clients.
     *
     * @param id  The document id to remove.
     */
    public static void remove(String id)
    {
        map.remove(id);
    }

/* Code for the individual documents */

    public String id;
    public boolean syncMode = false;
    private Collection<Client> clients = new LinkedList<Client>();

    /**
     * Construct a new <code>Document</code>.
     *
     * This gets run on a call to <code>get</code> with a new id.
     *
     * @param id  The id of this document is known by.
     */
    private Document(String id)
    {
        this.id = id;
    }

    /**
     * Add a client to the list.
     *
     * @param c  The client.
     */
    public synchronized void subscribe(Client c)
    {
        clients.add(c);
    }

    /**
     * Remove a client from the list.
     *
     * If the client list is empty, this will cause this <code>Document</code>
     * to remove itself from the static <code>Map</code> of documents and thus
     * self-destruct.
     *
     * @param c  The client
     */
    public synchronized void unsubscribe(Client c)
    {
        clients.remove(c);
        if (clients.isEmpty())
            Document.remove(this.id);
    }

    /**
     * Run an action for each client in turn.
     *
     * XXX: Clients may not be unsubscribed during the course of this operation
     *
     * This method is synchronized, so that an action runs on all clients
     * before any other actions run.
     *
     * @param a  The action to run.
     */
    public synchronized void forEachClient(ClientAction a)
    {
        for (Client c : clients)
        {
            a.action(c);
        }
    }

    /**
     * Send a bit of JSON to each client that is subscribed.
     *
     * Multiple calls to this method will ensure that the clients
     * all recieve the same messages in the same order.
     *
     * @param msg  The JSON to send.
     */
    public void sendEachClient(final JSONObject msg)
    {
        this.forEachClient( new ClientAction (){

            public void action (Client c)
            {
                c.send(msg);
            }

        });
    }
    
    /**
     * Construct the {"type": "users"} message, and send it to every client.
     *
     * This method is thread-safe, i.e. the list of clients sent will be the
     * list of subscribed clients at the time of invokation.
     */
    public synchronized void sendUserList() throws JSONException
    {
        final JSONObject user_list = new JSONObject();
        user_list.put("type", "users");
        user_list.put("users", new JSONArray());

        this.forEachClient( new ClientAction () {
            public void action (Client client)
            {
                try
                {
                    user_list.append("users", client.userInfo());
                }
                catch(JSONException e)
                {
                    System.out.println("Could not create userinfo:" + e);
                }
            }

        });

        this.sendEachClient(user_list);
    }
}
